<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4.10.1-立即执行的 watch 与回调执行时机</title>
</head>

<body>
    <div id="container"></div>
</body>
<script>
    /**
     * 立即执行的回调函数
     * - 当 immediate 选项存在并且为 true 时，回调函数会在该 watch 创建时立刻执行一次
     * - 把 scheduler 调度函数封装为一个通用函数，分别在初始化和变更时执行它
     * - 第一次回调执行时没有所谓的旧值，因此此时回调函数的 oldValue 值为 undefined
     * 
     * */

    // 存储副作用函数的桶
    const bucket = new WeakMap()
    // 原始数据
    const data = { foo: 1, bar: 2 }
    // 对原始数据的代理
    const obj = new Proxy(data, {
        // 拦截读取操作
        get(target, key) {
            // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
            track(target, key)
            // 返回属性值
            return target[key]
        },
        // 拦截设置操作
        set(target, key, newVal) {
            // 设置属性值
            target[key] = newVal
            // 把副作用函数从桶里取出并执行
            trigger(target, key)
        }
    })

    // 在 get 拦截函数内调用 track 函数追踪变化
    function track(target, key) {
        // 没有 activeEffect，直接 return
        if (!activeEffect) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        // 把当前激活的副作用函数添加到依赖集合 deps 中
        deps.add(activeEffect)
        // deps 就是一个与当前副作用函数存在联系的依赖集合
        // 将其添加到 activeEffect.deps 数组中
        activeEffect.deps.push(deps)
    }
    // 在 set 拦截函数内调用 trigger 函数触发变化
    function trigger(target, key) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同，则不触发执行
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
        effectsToRun.forEach(effectFn => {
            // 如果一个副作用函数存在调度器，则调用该调度器，并将副作用函数作为参数传递
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                // 否则直接执行副作用函数（之前的默认行为）
                effectFn()
            }
        })
    }

    // 用一个全局变量存储被注册的副作用函数
    let activeEffect
    // effect 栈
    // 个人理解，始终都会留着一个是最先执行的
    const effectStack = []

    function effect(fn, options = {}) {
        const effectFn = () => {
            cleanup(effectFn)
            // 当调用 effect 注册副作用函数时，将副作用函数赋值给 activeEffect
            activeEffect = effectFn
            // 在调用副作用函数之前将当前副作用函数压入栈中
            effectStack.push(effectFn)
            // 将 fn 的执行结果存储到 res 中
            const res = fn()
            // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并把 activeEffect 还原为之前的值
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            // 将 res 作为 effectFn 的返回值
            return res;
        }
        // 将 options 挂载到 effectFn 上
        effectFn.options = options
        // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
        effectFn.deps = []
        // 只有非 lazy 的时候，才执行
        if (!options.lazy) {
            // 执行副作用函数
            effectFn()
        }
        // 将副作用函数作为返回值返回
        return effectFn
    }

    function cleanup(effectFn) {
        // 遍历 effectFn.deps 数组
        for (let i = 0; i < effectFn.deps.length; i++) {
            // deps 是依赖集合
            const deps = effectFn.deps[i]
            // 将 effectFn 从依赖集合中移除
            deps.delete(effectFn)
        }
        // 最后需要重置 effectFn.deps 数组
        effectFn.deps.length = 0
    }

    // 定义一个任务队列
    const jobQueue = new Set()
    // 使用 Promise.resolve() 创建一个 promise 实例，我们用它将一个任务添加到微任务队列
    const p = Promise.resolve()
    // 一个标志代表是否正在刷新队列
    let isFlushing = false
    function flushJob() {
        // 如果队列正在刷新，则什么都不做
        if (isFlushing) return
        // 设置为 true，代表正在刷新
        isFlushing = true
        // 在微任务队列中刷新 jobQueue 队列
        // 微任务会在宏任务之后执行
        p.then(() => {
            jobQueue.forEach(job => job())
        }).finally(() => {
            // 结束后重置 isFlushing
            isFlushing = false
        })
    }

    function computed(getter){
        // value 用来缓存上一次计算的值
        let value
        // dirty 标志，用来标识是否需要重新计算值，为 true 则意味着“脏”，需要计算
        let dirty = true

        // 把 getter 作为副作用函数，创建一个 lazy 的 effect
        const effectFn = effect(getter, {
            lazy: true,
            // 添加调度器，在调度器中将 dirty 重置为 true
            scheduler() {
                dirty = true
                // 当计算属性依赖的响应式数据变化时，手动调用 trigger 函数触发响应
                trigger(obj, 'value')
            }
        })
        const obj = {
            // 当读取 value 时才执行 effectFn
            get value() {
                // 只有“脏”时才计算值，并将得到的值缓存到 value 中
                if (dirty) {
                    value = effectFn()
                    // 将 dirty 设置为 false，下一次访问直接使用缓存到 value 中的值
                    dirty = false
                }
                // 当读取 value 时，手动调用 track 函数进行追踪
                track(obj, 'value')
                return value
            }
        }
        return obj
    }

    function traverse(value, seen = new Set()) {
        // 如果要读取的数据是原始值，或者已经被读取过了，那么什么都不做
        if (typeof value !== 'object' || value === null || seen.has(value)) return
        // 将数据添加到 seen 中，代表遍历地读取过了，避免循环引用引起的死循环
        seen.add(value)
        // 暂时不考虑数组等其他结构
        // 假设 value 就是一个对象，使用 for...in 读取对象的每一个值，并递归地调用 traverse 进行处理
        for (const k in value) {
            traverse(value[k], seen)
        }
        return value
    }
    
    // watch 函数接收两个参数，source 是响应式数据，cb 是回调函数
    function watch(source, cb, options = {}) {
        // 定义 getter
        let getter
        // 如果 source 是函数，说明用户传递的是 getter，所以直接把 source 赋值给 getter
        if (typeof source === 'function') {
            getter = source
        } else { 
            // 否则按照原来的实现调用 traverse 递归地读取
            getter = () => traverse(source)
        }

        // 定义旧值与新值
        let oldValue, newValue

        // 提取 scheduler 调度函数为一个独立的 job 函数
        const job = () => { // 新增 job 函数
            newValue = effectFn()
            cb(newValue, oldValue)
            oldValue = newValue
        }

        const effectFn = effect(
            // 触发读取操作，从而建立联系
            () => getter(),
            {
                lazy: true,
                scheduler: job
            }
        )
        if (options.immediate) { // 新增 immediate 判断
            // 当 immediate 为 true 时立即执行 job，从而触发回调执行
            job()
        } else {
            // 手动调用副作用函数，拿到的值就是旧值
            oldValue = effectFn()
        }
    }

    
    watch(
        // getter 函数
        ()=>{
            return obj.foo
        }, 
        // 回调函数
        (newValue, oldValue) => {
            console.log(newValue, oldValue)
        },
        {   // 新增 测试
            // 回调函数会在 watch 创建时立即执行一次
            immediate: true
        }
    )

    // 修改响应数据的值，会导致回调函数执行
    obj.foo++
</script>

</html>